﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Cofe.Core;
using Cofe.Core.Filter;
using Cofe.Core.Implements;
using Cofe.Core.Property;
using Cofe.Core.Service;
using Cofe.Core.Utils;
using Cofe.Core.Validater;
using Cofe.Data.Filter;
using Cofe.Data.Utils;

namespace Cofe.Data
{
    public class EFRepository : IRepository
    {
        [Export(typeof(ICofeService))]
        [ServicePriority(ServicePriorityAttribute.DefaultPriority_COFEDB)]
        public class Factory : IRepositoryFactory
        {
            public static int Count = 0;

            public IRepository CreateRepository()
            {
                Count++;
                return new EFRepository();
            }
        }

        #region Constructor

        public EFRepository()
        {
            _context = new CofeDbContext();
            _databaseEntryFactory = new EFDatabaseEntryFactory(this);
            EventRepository = new EFEventRepository(this);
        }

        #endregion

        #region Methods

        public void Dispose()
        {
            if (_context != null)
            {
                Factory.Count--;
                _context.Dispose();
                _context = null;
            }
        }

        #region Helpers

        public List<Guid> FindId(FilterCollections filters)
        {
            var query = EntryFilterExpressionFactory.ConstructExpression(filters);
            return _context.LocalOrDatabase(query).Select(e => e.Id).Distinct().ToList();
        }

        public IEnumerable<DatabaseEntry> FindDbEntry(FilterCollections filters)
        {
            var entryIds = FindId(filters);
            return from entryId in entryIds
                   let e = _context.DBEntries
                       //.Include(de => de.Implementations)                       
                       .FirstOrDefault(e => e.Id == entryId)
                   where e != null
                   select e;
        }

        public IEnumerable<DatabaseUpdateEntry> FindExpired(UpdateType updateType, int count)
        {
            if (count <= 0)
                count = 1000;
            IEnumerable<DatabaseUpdateEntry> query1 = null;
            IEnumerable<DatabaseUpdateEntry> query2 = null;

            if (updateType.HasFlag(UpdateType.updateExpired))
            {
                DateTime entryExpiredTime = DateTime.UtcNow.Subtract(CofeServices.CofeSettings.CacheExpireTime);
                query2 = Context.DBEntries.Where(de => de.LastCacheTime < entryExpiredTime)
                                .OrderBy(de => de.LastCacheTime)
                                .Take(count).AsEnumerable()
                                .Select(de => new DatabaseUpdateEntry()
                                {
                                    Id = de.Id,
                                    ParseName = de.ParseName,
                                    ListingExpired = false,
                                    EntryExpired = true
                                });
            }


            if (updateType.HasFlag(UpdateType.updateListing))
            {
                DateTime listExpiredTime = DateTime.UtcNow.Subtract(CofeServices.CofeSettings.ListExpireTime);
                string idStr = Cofe.Core.PropertyDefinitions.CofeDirDefinitions.BeginId.ToString();
                query1 = Context.DBEntries.AsQueryable().Where(de => de.LastListTime < listExpiredTime &&
                    de.SupportDefinitions.Contains("," + idStr + ","))
                    .OrderBy(de => de.LastListTime).AsEnumerable()
                    .Select(de => new DatabaseUpdateEntry()
                    {
                        Id = de.Id,
                        ParseName = de.ParseName,
                        ListingExpired = true,
                        EntryExpired = false
                    });
            }

            if (query1 == null && query2 == null)
                return new List<DatabaseUpdateEntry>();

            var query = query2 != null && query1 != null ? query2.Union(query1) :
                        query2 == null ? query1 : query2;

            if (count != -1)
                query = query.Take(count);


            return query.Distinct();
        }


        DatabaseEntry[] add(bool forceUpdate, params IPropertyHost[] propertyHosts)
        {
            List<DatabaseEntry> retList = new List<DatabaseEntry>();

            DatabaseEntry[] dbEntries = null;

            if (!forceUpdate && CofeServices.CofeSettings.CacheUpdateOptions.HasFlag(CacheUpdateOptions.activeUpdate))
            {
                dbEntries = DatabaseEntryFactory.FindOrCreateDatabaseEntry(_context, propertyHosts).Select(pp => pp.Item2).ToArray();
            }
            else
            {
                dbEntries = DatabaseEntryFactory.CreateFullDatabaseEntry(_context, propertyHosts).ToArray();
            }

            foreach (var dbEntry in dbEntries)
                CofeServices.Logger.Log("EFR: added {0} to EF (Local)", dbEntry.ParseName);

            return dbEntries;
        }

        DatabaseEntry[] update(IPropertyHost[] propertyHosts)
        {
            return DatabaseEntryFactory.CreateFullDatabaseEntry(_context, propertyHosts).ToArray();
        }

        #endregion

        public DatabaseEntry[] RefreshListing(IPropertyHost parentPropertyHost)
        {
            string parentParseName = parentPropertyHost.Behaviors.GetProperty<string>(CofeProperties.FullParseName);
            try
            {
                var subEntriesPH =
                    parentPropertyHost.Behaviors.Invoke<IEnumerable<ICofeSystemInfo>>(CofeDirectoryProperties.ListAsync,
                            new ParameterDic() { EnableCache = false }).Select(e => e.Properties);

                return AddOrUpdate(false, true, subEntriesPH.ToArray());
            }
            catch (Exception ex)
            {
                CofeServices.Logger.Log(ex);
                return new DatabaseEntry[] { };
            }
            finally
            {
                Refresh(FilterCollections.FromParseName(parentParseName), false, true);
                SaveChanges();
            }

        }


        public DatabaseEntry[] AddOrUpdate(bool forceUpdate, bool isDirectoryUpdate,
            params IPropertyHost[] propertyHosts)
        {
            List<DatabaseEntry> retList = new List<DatabaseEntry>();
            List<IPropertyHost> newEntries = new List<IPropertyHost>();
            List<IPropertyHost> updateEntries = new List<IPropertyHost>();
            List<string> deleteEntries = new List<string>();

            if (!forceUpdate)
                foreach (var ph in propertyHosts)
                {
                    if (ph.Behaviors.IsPropertySupported(CofeProperties.CofeAttributes) &&
                        ph.Behaviors.GetProperty<CofeAttributes>(CofeProperties.CofeAttributes).HasFlag(CofeAttributes.VirtualItems))
                    {
                        forceUpdate = true;
                        break;
                    }
                }

            if (isDirectoryUpdate)
            {
                string parentParsePath =
                    propertyHosts[0].Behaviors.GetProperty<string>(CofeProperties.ParentParseName);
                deleteEntries = _context.DBEntries.Where(de => de.Parent != null
                    && de.Parent.ParseName == parentParsePath)
                    .Select(de => de.ParseName).ToList();
                //To-Do: Fix RootEntryFilterExpression to use ParseName instead of Parent.ParseName, which crash when Parent = null
                //deleteEntries = FindDbEntry(FilterCollections.FromParentParseName(parentParsePath))
                //    .Select(de => de.ParseName).ToList();
            }


            foreach (var ph in propertyHosts)
            {
                string parsePath = ph.Behaviors.GetProperty<string>(CofeProperties.FullParseName);
                if (deleteEntries.Contains(parsePath))
                    deleteEntries.Remove(parsePath);

                if (Context.LocalOrDatabase<DatabaseEntry>(de => de.ParseName == parsePath).Any())
                    updateEntries.Add(ph);
                else newEntries.Add(ph);
            }

            foreach (var d in deleteEntries)
                Remove(d);

            try
            {
                propertyHosts.ChangeState(PropertyHostState.ORMUpdating);

                if (newEntries.Count > 0)
                    retList.AddRange(add(forceUpdate, newEntries.ToArray()));
                if (updateEntries.Count > 0)
                    retList.AddRange(update(updateEntries.ToArray()));
            }
            finally
            {
                propertyHosts.ChangeState(PropertyHostState.Default);
            }
            return retList.ToArray();
        }


        public void ChangeValue<T>(DatabaseEntry dbEntry, Expression<Func<DatabaseEntry, T>> propertySelector, T value)
        {
            _context.Entry(dbEntry).Property(propertySelector).CurrentValue = value;
        }

        //public void SetValue<T>(DatabaseEntry dbEntry, Expression<Func<CofeSystemInfo, T>> propertySelector, T value)
        //{

        //}

        public T GetValue<T>(DatabaseEntry dbEntry, Expression<Func<CofeSystemInfo, T>> propertySelector)
        {
            return _context.Entry(dbEntry).Collection(de => de.Implementations)
                .Query().Select(propertySelector).FirstOrDefault();
        }

        public bool Exists(FilterCollections filters)
        {
            var query = EntryFilterExpressionFactory.ConstructExpression(filters);
            //return _context.Entries.Where(query).Any();
            return _context.LocalOrDatabase<CofeSystemInfo>(query).Any();
        }

        public void Remove(Guid id)
        {
            _context.Database.ExecuteSqlCommand("DELETE FROM dbo.DatabaseEntry where Id = '" + id.ToString() + "%'");
        }

        public void Remove(string parseName)
        {
            var delList = _context.DBEntries.Where(de => de.ParseName == parseName || de.ParseName.StartsWith(parseName + "\\"));
            foreach (var d in delList)
                _context.DBEntries.Remove(d);

            //_context.Database.ExecuteSqlCommand(
            //    "DELETE FROM dbo.DatabaseEntry where ParseName LIKE '@parseName%'", parseName);
        }

        public void Remove(FilterCollections filters)
        {
            var dbEntries = FindDbEntry(filters).ToList();
            if (dbEntries.Any())
            {
                foreach (var dbEntry in dbEntries)
                    _context.DBEntries.Remove(dbEntry);
            }
        }

        public void Refresh(FilterCollections filters, bool refreshCacheTime = true, bool refreshListTime = true)
        {
            var entries = FindDbEntry(filters).ToArray();
            foreach (var dbEntry in entries)
            {
                if (refreshCacheTime)
                    _context.Entry(dbEntry).Property(de => de.LastCacheTime).CurrentValue = DateTime.UtcNow;
                if (refreshListTime)
                    _context.Entry(dbEntry).Property(de => de.LastListTime).CurrentValue = DateTime.UtcNow;

            }
        }

        public void Refresh(Guid id, bool refreshCacheTime = true, bool refreshListTime = true)
        {
            Refresh(FilterCollections.FromGuid(id), refreshCacheTime, refreshListTime);
        }

        private HashTag getHashTag(string tagName)
        {
            HashTag retVal = _context.LocalOrDatabase<HashTag>(t => t.TagName == tagName)
                .Include(ht => ht.AppliedEntries).FirstOrDefault();
            if (retVal == null)
            {
                retVal = new HashTag() { TagName = tagName };
                _context.Entry(retVal).State = System.Data.EntityState.Added;
            }
            //else _context.Entry(retVal).State = System.Data.EntityState.Modified;
            return retVal;
        }

        private HashTag[] checkTagExists(string[] tags)
        {
            List<HashTag> retVal = new List<HashTag>();
            if (tags != null)
            {
                foreach (var tag in tags)
                    retVal.Add(getHashTag(tag));
                _context.SaveChanges();
            }
            return retVal.ToArray();
        }

        public int ModifyHashTag(FilterCollections filters, string[] addTag, string[] removeTag)
        {
            var appliedIds = FindId(filters);

            if (addTag != null)
                foreach (var tag in addTag)
                {
                    HashTag ht = getHashTag(tag);
                    foreach (var id in appliedIds)
                        ht.AppliedEntries.Add(_context.DBEntries.Include(de => de.HashTags).First(de => de.Id == id));
                    _context.ChangeTracker.DetectChanges();
                }

            if (removeTag != null)
                foreach (var tag in removeTag)
                {
                    HashTag ht = getHashTag(tag);
                    foreach (var id in appliedIds)
                    {
                        var dbEntry = _context.DBEntries.Include(de => de.HashTags).First(de => de.Id == id);
                        if (dbEntry.HashTags.Contains(ht))
                            dbEntry.HashTags.Remove(ht);
                    }

                    _context.ChangeTracker.DetectChanges();
                }

            Refresh(filters, true, false);
            return appliedIds.Count();
        }


        public void Expire(FilterCollections filters)
        {
            var entries = FindDbEntry(filters);
            foreach (var dbEntry in entries)
            {
                _context.Entry(dbEntry).Property(de => de.LastCacheTime).CurrentValue = CofeConstants.MinDateTime;
                _context.Entry(dbEntry).Property(de => de.LastListTime).CurrentValue = CofeConstants.MinDateTime;
            }
        }

        public void Expire(Guid id)
        {
            Expire(FilterCollections.FromGuid(id));
        }



        public IEnumerable<IPropertyHost> FindAll(FilterCollections filters)
        {
            //var paginateLookup = filters.OptionFilters.FirstOrDefault(f => f is PaginateOptionFilter) as PaginateOptionFilter;
            //To-Do: FindAll<T> calls UpdateEntry for updating properties, shouldnt require in this case
            IEnumerable<Guid> entryIds = this.FindAll<CofeSystemInfo>(filters).Select(e => e.Id).Distinct().ToList();
            //if (paginateLookup != null)
            //    entryIds = DBFilterUtils.processPaginate(entryIds, paginateLookup);
            return from entryId in entryIds
                   select
                       DatabaseEntryFactory.UpdateImpl(
                            updateImplBug(
                            _context.DBEntries.FirstOrDefault(e => e.Id == entryId)
                            )
                            );
        }

        public int Count(FilterCollections filters)
        {
            var query = EntryFilterExpressionFactory.ConstructExpression(filters);
            return _context.Entries.Where(query).Select(e => e.Id).Distinct().Count();
        }

        private DatabaseEntry updateImplBug(DatabaseEntry dbEntry)
        {
            //Console.WriteLine(dbEntry.Implementations.Count);
            if (!(_context.Entry(dbEntry).Collection(de => de.Implementations)).IsLoaded)
                dbEntry.Implementations =
                    _context.Entry(dbEntry).Collection(de => de.Implementations)
                        .Query().Select(e => e).ToList();
            //Console.WriteLine(dbEntry.Implementations.Count);
            return dbEntry;
        }

        public IEnumerable<T> FindAll<T>(FilterCollections filters) where T : ICofeSystemInfo
        {
            var queryExpression = EntryFilterExpressionFactory.ConstructExpression(filters);
            var query = _context.Entries.Include(e => e.DatabaseEntry)
                .Where(queryExpression);

            var impls = (DBFilterUtils.UpdateOptionFilterExpressions(query, filters.OptionFilters, true, true))
                        .AsEnumerable().Where(e => e is T).Select(e => e as CofeSystemInfo).ToList();
            return from impl in impls
                   let dbEntry = _context.DBEntries
                   .Include(e => e.ExtendedProperties)
                       //.Include(e => e.Implementations)
                   .Include(e => e.Parent)
                   .Include(e => e.HashTags)
                   .Include(e => e.EntryTypeInfo)
                   .Include(e => e.EntryTypeInfo.Implementations)
                   .Include(e => e.EntryTypeInfo.ExtendedProperties)
                   .First(e => e.Id == impl.Id)
                   select DatabaseEntryFactory.UpdateImpl<T>(
                        updateImplBug(impl.DatabaseEntry),
                        (T)(object)impl);
        }

        public void SaveChanges()
        {
            if (_context == null)
                throw new ArgumentException("Context already disposed.");
            int changeCount = _context.ChangeTracker.Entries().Count();
            try
            {
                _context.SaveChanges();
                CofeServices.Logger.Log("EFR: SaveChanges() called with {0} changes.", changeCount);
            }
            catch (Exception ex)
            {
                //foreach (var e in _context.ChangeTracker.Entries())
                //    if (e.Entity is DatabaseEntry)
                //        Console.WriteLine((e.Entity as DatabaseEntry).ParseName);
                CofeServices.Logger.Log(ex);
            }

        }

        #endregion

        #region Data

        IServiceLocater _serviceLocater { get; set; }
        CofeDbContext _context;
        IDatabaseEntryFactory _databaseEntryFactory;
        IEntryFilterExpressionFactory _entryFilterExpressionFactory = null;

        #endregion

        #region Public Properties

        public CofeDbContext Context { get { return _context; } }

        public IServiceLocater ServiceLocater
        {
            get { return _serviceLocater ?? CofeServices.ServiceLocater; }
            set { _serviceLocater = value; }
        }

        public IEventRepository EventRepository
        {
            get;
            private set;
        }

        public IDatabaseEntryFactory DatabaseEntryFactory { get { return _databaseEntryFactory; } }
        public IEntryFilterExpressionFactory EntryFilterExpressionFactory
        {
            get { return _entryFilterExpressionFactory ?? (_entryFilterExpressionFactory = ServiceLocater.FindService<IEntryFilterExpressionFactory>()); }
        }
        #endregion


















    }
}
